/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mapbox.ohos.gestures;

import ohos.app.Context;

import java.util.HashSet;
import java.util.Set;

import static com.mapbox.ohos.gestures.OhosGesturesManager.GESTURE_TYPE_SHOVE;

/**
 * Gesture detector handling shove gesture.
 */
public class ShoveGestureDetector extends ProgressiveGesture<ShoveGestureDetector.OnShoveGestureListener> {
    private static final Set<Integer> HANDLED_TYPE = new HashSet<>();

    static {
        HANDLED_TYPE.add(GESTURE_TYPE_SHOVE);
    }

    private float maxShoveAngle;
    private float pixelDeltaThreshold;
    float deltaPixelsSinceStart;
    float deltaPixelSinceLast;

    public ShoveGestureDetector(Context context, OhosGesturesManager gesturesManager) {
        super(context, gesturesManager);
    }

    @Override
    protected Set<Integer> provideHandledTypes() {
        return HANDLED_TYPE;
    }

    /**
     * Listener for shove callbacks.
     */
    public interface OnShoveGestureListener {
        /**
         * Indicates that the shove gesture started.
         *
         * @param detector this detector
         * @return true if you want to receive subsequent {@link #onShove(ShoveGestureDetector, float, float)} callbacks,
         * false if you want to ignore this gesture.
         */
        boolean onShoveBegin(ShoveGestureDetector detector);

        /**
         * Called for every shove change during the gesture.
         *
         * @param detector this detector
         * @param deltaPixelsSinceLast pixels delta change since the last call
         * @param deltaPixelsSinceStart pixels delta change since the start of the gesture
         * @return true if the gesture was handled, false otherwise
         */
        boolean onShove(ShoveGestureDetector detector, float deltaPixelsSinceLast, float deltaPixelsSinceStart);

        /**
         * Indicates that the shove gesture ended.
         *
         * @param velocityX velocityX of the gesture in the moment of lifting the fingers
         * @param velocityY velocityY of the gesture in the moment of lifting the fingers
         * @param detector this detector
         */
        void onShoveEnd(ShoveGestureDetector detector, float velocityX, float velocityY);
    }

    /**
     * SimpleOnShoveGestureListener
     */
    public static class SimpleOnShoveGestureListener implements OnShoveGestureListener {
        /**
         * onShoveBegin
         *
         * @param detector this detector
         * @return true
         */
        @Override
        public boolean onShoveBegin(ShoveGestureDetector detector) {
            return true;
        }

        /**
         * onShove
         *
         * @param detector this detector
         * @param deltaPixelsSinceLast pixels delta change since the last call
         * @param deltaPixelsSinceStart pixels delta change since the start of the gesture
         * @return false
         */
        @Override
        public boolean onShove(ShoveGestureDetector detector, float deltaPixelsSinceLast, float deltaPixelsSinceStart) {
            return false;
        }

        /**
         * onShoveEnd
         *
         * @param detector this detector
         * @param velocityX velocityX of the gesture in the moment of lifting the fingers
         * @param velocityY velocityY of the gesture in the moment of lifting the fingers
         */
        @Override
        public void onShoveEnd(ShoveGestureDetector detector, float velocityX, float velocityY) {
            // No Implementation
        }
    }

    /**
     * analyzeMovement
     *
     * @return boolean
     */
    @Override
    protected boolean analyzeMovement() {
        super.analyzeMovement();

        deltaPixelSinceLast = calculateDeltaPixelsSinceLast();
        deltaPixelsSinceStart += deltaPixelSinceLast;

        if (isInProgress() && deltaPixelSinceLast != 0) {
            return listener.onShove(this, deltaPixelSinceLast, deltaPixelsSinceStart);
        } else if (canExecute(GESTURE_TYPE_SHOVE)) {
            if (listener.onShoveBegin(this)) {
                gestureStarted();
                return true;
            }
        }

        return false;
    }

    /**
     * canExecute
     *
     * @param invokedGestureType
     * @return boolean
     */
    @Override
    protected boolean canExecute(int invokedGestureType) {
        return Math.abs(deltaPixelsSinceStart) >= pixelDeltaThreshold
            && super.canExecute(invokedGestureType);
    }

    /**
     * isSloppyGesture
     *
     * @return boolean
     */
    @Override
    protected boolean isSloppyGesture() {
        return super.isSloppyGesture() || !isAngleAcceptable();
    }

    /**
     * gestureStopped
     */
    @Override
    protected void gestureStopped() {
        super.gestureStopped();
        listener.onShoveEnd(this, velocityX, velocityY);
    }

    /**
     * reset
     */
    @Override
    protected void reset() {
        super.reset();
        deltaPixelsSinceStart = 0;
    }

    /**
     * isAngleAcceptable
     *
     * @return boolean
     */
    private boolean isAngleAcceptable() {
        MultiFingerDistancesObject distancesObject =
            pointersDistanceMap.get(new PointerDistancePair(pointerIdList.get(0), pointerIdList.get(1)));
        // Takes values from 0 to 180
        double angle = Math.toDegrees(Math.abs(Math.atan2(
            distancesObject.getCurrFingersDiffY(), distancesObject.getCurrFingersDiffX())));

        return angle <= maxShoveAngle || 180f - angle <= maxShoveAngle;
    }

    /**
     * calculateDeltaPixelsSinceLast
     *
     * @return float
     */
    private float calculateDeltaPixelsSinceLast() {
        float py0 = getPreviousEvent().getPointerPosition(pointerIdList.get(0)).getY();
        float py1 = getPreviousEvent().getPointerPosition(pointerIdList.get(1)).getY();
        float prevAverageY = (py0 + py1) / 2.0f;

        float cy0 = getCurrentEvent().getPointerPosition(pointerIdList.get(0)).getY();
        float cy1 = getCurrentEvent().getPointerPosition(pointerIdList.get(1)).getY();
        float currAverageY = (cy0 + cy1) / 2.0f;

        return currAverageY - prevAverageY;
    }

    /**
     * Returns vertical pixel delta change since the start of the gesture.
     *
     * @return pixels delta change since the start of the gesture
     */
    public float getDeltaPixelsSinceStart() {
        return deltaPixelsSinceStart;
    }

    /**
     * Returns last vertical pixel delta change
     * calculated in {@link OnShoveGestureListener#onShove(ShoveGestureDetector, float, float)}.
     *
     * @return pixels delta change since the last call
     */
    public float getDeltaPixelSinceLast() {
        return deltaPixelSinceLast;
    }

    /**
     * Get the delta pixel threshold required to qualify it as a shove gesture.
     *
     * @return delta pixel threshold
     */
    public float getPixelDeltaThreshold() {
        return pixelDeltaThreshold;
    }

    /**
     * Set the delta pixel threshold required to qualify it as a shove gesture.
     * <p>
     * We encourage to set those values from dimens to accommodate for various screen sizes.
     *
     * @param pixelDeltaThreshold delta threshold
     */
    public void setPixelDeltaThreshold(float pixelDeltaThreshold) {
        this.pixelDeltaThreshold = pixelDeltaThreshold;
    }

    /**
     * Set the delta dp threshold required to qualify it as a shove gesture.
     *
     * @param pixelDeltaThresholdDimen delta threshold
     */
    public void setPixelDeltaThresholdResource(int pixelDeltaThresholdDimen) {
        setPixelDeltaThreshold(Utils.fpToPx(pixelDeltaThresholdDimen, context));
    }

    /**
     * Get the maximum allowed angle between fingers, measured from the horizontal line, to qualify it as a shove gesture.
     *
     * @return maximum allowed angle
     */
    public float getMaxShoveAngle() {
        return maxShoveAngle;
    }

    /**
     * Set the maximum allowed angle between fingers, measured from the horizontal line, to qualify it as a shove gesture.
     *
     * @param maxShoveAngle maximum allowed angle
     */
    public void setMaxShoveAngle(float maxShoveAngle) {
        this.maxShoveAngle = maxShoveAngle;
    }
}
